package TwoClientGameConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;

import org.jdom.JDOMException;

/**
 * The client of a server/client relay. The specific job of this
 * class is to send and receieve messages to a server. It has transmission
 * checks in place to make sure the message arrived at its destination. 
 * (However a redelivery is only attempted 6 times then it is dropped.)
 * This class should not be modified. To utilize the client 
 * write a custom protocol that implements TCGClientProtocol.
 * The class implements Runnable so it can be run in a new thread.
 * @author Jace Ferguson
 * @filename TCGClient.java
 */
public class TCGClient implements Runnable {
	private Socket socket;
	private PrintWriter os;
	private BufferedReader is;
	private TCGClientProtocol protocol;
	private TCGMessage lastMessage;
	private Integer attemptedSends = 0;
	private LinkedList<TCGMessage> outGoingMessages;
	private Integer messageId = 0;
	
	/**
	 * Constructor used to create an instance of the client. It attempts to 
	 * connect to the socket at the given address and port. Actual processing
	 * occurs when the thread is started by the run method.
	 * @param address	String	Host to the server socket.
	 * @param port	int	Port number for the string.
	 * @throws TCGGeneralException
	 */
	public TCGClient(String address, int port) throws TCGGeneralException
	{
		assert(address != null);
		this.outGoingMessages = new LinkedList<TCGMessage>();
		try{
			this.socket = new Socket(address, port);
			this.os = new PrintWriter(new OutputStreamWriter(this.socket.getOutputStream()));
			this.is = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
		}
		catch(UnknownHostException e)
		{
			throw new TCGGeneralException("Unknown Host Exception");
		}
		catch(IOException e)
		{
			throw new TCGGeneralException("IO Client Connection Exception");
		}
	}
	
	/**
	 * On each iteration check for data waiting to be read.
	 * If there is data, handle the data.
	 */
	public void doIteration()
	{
		String messageRecieved = "";
		String line;
		try{
			while((line = this.is.readLine()) != null)
			{	
				messageRecieved += line;
				//Without this, readLine blocks and the message gets stopped. 
				//This waits until the end tag of the XML has been found then passes the 
				//message on to be processed.
				if(messageRecieved.substring(messageRecieved.length() - 13, messageRecieved.length()).equals("</TCGMessage>"))
				{
					break;
				}
			}
			//create a message object from the transmitted message.
			TCGMessage message = TCGMessageReader.ParseMessage(messageRecieved);
			//Send a reply that the message was received correctly.
			this.replyMessageValid(message);
			//Send the message to the switchboard for processing.
			this.switchBoard(message);
		}
		catch(IOException e) //THe message couldn't be read. Request copy.
		{
			this.requestLastMessageCopy();
		}
		catch(JDOMException e) //Parsing of the message failed meaning it was garbled. Request copy.
		{
			this.requestLastMessageCopy();
		}
	}
	
	/**
	 * Gets the current message id. A message id is required for
	 * sending a message.
	 * @return	Integer The current message id.
	 */
	public Integer getMessageId()
	{
		Integer messageId = this.messageId;
		this.incrementMessageCount();
		return messageId;
	}
	
	/**
	 * Get the client protocol.
	 * @return 	TCGClientProtocol protocol
	 */
	public TCGClientProtocol getProtocol()
	{
		return this.protocol;
	}
	
	/**
	 * Each message is given a unique id up to 5000 then
	 * the id is reset. Hopefully, no application will have more
	 * than 5000 active messages...
	 */
	private void incrementMessageCount()
	{
		if(this.messageId == 5000)
		{
			this.messageId = -1;
		}
		++this.messageId;
	}
	
	/**
	 * The client has decided to close the connection.
	 * Immediate quit will send the message without even if
	 * the client is waiting to receive a reply.
	 * Non immediate waits until the message reaches the head of the queue.
	 * @param immediateQuit	boolean	rather or not to quit without adding
	 * 								the message to the send queue.
	 */
	public void quit(boolean immediateQuit)
	{
		TCGMessage quitMessage = new TCGMessage(this.getMessageId());
		quitMessage.setMessageId(TCGMessage.MESSAGE_TYPE_HIGH);
		quitMessage.setCommand("QUIT");
		if(immediateQuit)
		{
			this.writeMessage(quitMessage);
		}
		else
		{
			this.sendMessage(quitMessage);
		}
	}
	
	/**
	 * Create a message to send stating that a message was received successfully.
	 * @param message	TCGMessage object	Message that was received.
	 */
	public void replyMessageValid(TCGMessage message)
	{
		/*
		 * However, don't send a reply message to a 
		 * message replying to a successful transmission. 
		 * Leads to an infinite loop of messages and 
		 * lots of bandwidth.
		 */		
		if(!message.getCommand().equals("RMESSAGEVALID"))
		{
			TCGMessage replyMessage = new TCGMessage(this.getMessageId());
			replyMessage.setMessageType(TCGMessage.MESSAGE_TYPE_HIGH);
			replyMessage.setCommand("RMESSAGEVALID");
			this.writeMessage(replyMessage);
		}
	}
	
	/**
	 * Create a message to send to the server to request a copy of the
	 * last message that was transmitted.
	 */
	public void requestLastMessageCopy()
	{
		TCGMessage request = new TCGMessage(this.getMessageId());
		
		//Increase the bearing of the message.
		request.setMessageType(TCGMessage.MESSAGE_TYPE_HIGH);
		request.setCommand("REQUESTLAST");
		this.writeMessage(request);
	}
	
	/**
	 * Run is called when the thread is created. 
	 * This starts the client to actually start doing work.
	 */
	public void run()
	{
		while(true)
		{
			this.doIteration();
		}
	}
	
	/**
	 * Run the send queue to deliver outgoing messages.
	 */
	public void runQueueSend()
	{
		if(!this.outGoingMessages.isEmpty())
		{
			this.writeMessage(this.outGoingMessages.remove());
		}
	}
	
	/**
	 * Most messages should be sent by sending it to this method.
	 * This method then adds the message to the outgoing message queue and
	 * runs the queue.
	 * @param message	TCGMessage object	message to be sent.
	 */
	public void sendMessage(TCGMessage message)
	{
		this.outGoingMessages.add(message);
		this.runQueueSend();
	}
	
	/**
	 * Sends a message object to the client protocol for handling.
	 * @param message	TCGMessage	message to send to client protocol
	 */
	public void sendMessageToProtocol(TCGMessage message)
	{
		if(this.protocol != null)
		{
			this.protocol.handleMessage(message);
		}
	}
	
	/**
	 * Reset the class after the server has terminated its connection.
	 * Sends an error to the client.
	 * @param reason	String reason the server quit.
	 */
	public void serverKilled(String reason)
	{
		this.socket = null;
		this.os = null;
		this.is = null;
		this.lastMessage = null;
		this.attemptedSends = 0;
		this.protocol.handleError(new TCGConnectionException("The server terminated your connection. Here is what it had to say: " + reason));
	}
	
	/**
	 * Sets the user defined client protocol to use when handling messages
	 * and errors.
	 * @param protocol	TCGClientProtocol	protocol object.
	 */
	public void setProtocol(TCGClientProtocol protocol)
	{
		this.protocol = protocol;
	}
	
	/**
	 * The command processor of the client. It takes the message and determines what
	 * to do depending on the command.
	 * @command	CLOSECONNECTION		The server has closed the client.
	 * @command RMESSAGEVALID	A message that was sent to the server was received correctly.
	 * @command REQUESTLAST		The message is requesting that the last message be transmitted again.
	 * @command TXWARNING	The server has send a transmission warning. Alert the protocol.
	 * @command "Default"	Send the message to the protocol for handling.
	 * @param message	TCGMessage object	Message data retrieved from the server.
	 */
	public void switchBoard(TCGMessage message)
	{
		if(message.getCommand().equals("CLOSECONNECTION"))
		{
			this.serverKilled(message.getParameterValue("reason"));
		}
		else if(message.getCommand().equals("RMESSAGEVALID"))
		{
			this.lastMessage = null;
			this.attemptedSends = 0;
			this.runQueueSend();
		}
		else if(message.getCommand().equals("REQUESTLAST"))
		{
			if(this.attemptedSends == 6) //After six attempts it is time to give up.
			{
				TCGMessage warning = new TCGMessage(this.getMessageId());
				warning.setCommand("TXWARNING");
				warning.addParameter("warningMessage", "The client has failed to send a message 6 times. It was dropped. Crazy stuff may happen.");
				warning.setMessageType(TCGMessage.MESSAGE_TYPE_ERROR);
				this.writeMessage(warning);
				this.lastMessage = null;
				this.attemptedSends = 0;
				this.runQueueSend();
			}
			else //Try to submit again. We have not reached the submit limit.
			{
				this.lastMessage.setMessageType(TCGMessage.MESSAGE_TYPE_HIGH);
				this.writeMessage(this.lastMessage);	 
			}			 
		}
		else if(message.getCommand().equals("TXWARNING"))
		{
			this.sendMessageToProtocol(message);
		}
		else
		{
			this.sendMessageToProtocol(message);
		}

	}
	
	/**
	 * Send a message to the server.
	 * @param message	TCGMessage object	Message to be send to server.
	 */
	private void writeMessage(TCGMessage message)
	{
		String textMessage = TCGMessage.convertMessageToString(message);

		try{
			this.os.write(textMessage);
			this.os.flush();
			//Add the message to the queue and wait for reply
			if(message.getMessageType() != TCGMessage.MESSAGE_TYPE_ERROR || message.getMessageType() != TCGMessage.MESSAGE_TYPE_LOW)
			{
				this.lastMessage = message;
				this.attemptedSends++;
			}
		}catch (Exception e)
		{
			this.protocol.handleError(new TCGGeneralException("An error occured while transmitting message with id: " + message.getMessageId()));
		} 
	}
}


